Air Monitoring Data

API documentation for the air monitoring data published by Martinez Refining Company: how to call each endpoint, and how to download the same data by hand.

Looking for what a field means? Every field in every data set, with its type, is described in the data dictionary.

Four data sets are published, for four different purposes:

Data setWhat it isManual downloadAPI
Fenceline Monitoring Data Five-minute average pollutant concentrations, one record per instrument, parameter and averaging period, with detection limits, quality codes and instrument signal. CSV, XML CSV, JSON
GLM Data Five-minute average concentrations from the four ground level monitors operated under Air District Rules 9-1 and 9-2. Hydrogen sulfide at all four, sulfur dioxide at Ace Hardware. CSV, XML CSV, JSON
Monitoring Locations Where each analyzer, reflector, light source, met station and ground level monitor sits, and what it reports. Fixed positions, so there is no date range. CSV, GeoJSON, shapefile CSV, JSON, GeoJSON
Current Readings The single latest reading for every monitoring location and property, as shown on the public dashboard. Includes weather properties. None JSON

Fenceline Monitoring Data

Real-time data comes only from the API. The manual download stops one averaging period short of the present moment. To read the most recent five-minute period, use the API.

API

The API serves real-time data as well as historical. It will return the most recent completed averaging period, which the manual download will not.

Retrieving a long period

Nothing caps how much of the record you may retrieve, only how much of it arrives in one response. You do not need to work out window sizes, and asking for too much is not an error. Ask for the period you want; if it is larger than one response can carry, you get the start of it and the response tells you where to continue.

Every response reports the range it served and what to do next:

JSON fieldCSV response headerMeaning
startEpoch / endEpoch X-Start-Epoch / X-End-Epoch The range this response covers, which may be narrower than the one requested.
nextStartEpochX-Next-Start-Epoch Use this as start-epoch on your next request.
hasMoreX-Has-More true while data remains after this response. Loop until it reads false.
truncatedX-Truncated true when the requested range was larger than one response carries. requestedEndEpoch then reports the end the request asked for.
maxRangeSecondsX-Max-Range-Seconds The largest range one response will carry for the number of locations you requested.
earliestEpochNone The earliest reading available. A start-epoch before this moves forward to it, and the response says so in a note field instead of returning empty records for years the system did not run.

Retrieving everything takes no arithmetic. Ask for start-epoch=0 and keep going until hasMore reads false. You do not need to know when the record begins, how large a window to ask for, or how many requests it will take.

Example: Python

Downloads the complete record for every location and writes it to one CSV. Standard library only, no packages to install.

import csv, json, time, urllib.error, urllib.parse, urllib.request

BASE = "https://martinez.argos-scientific.com/api-fenceline-monitoring-data.php"

def get_page(url):
    """Fetch one page, waiting out the rate limit instead of failing."""
    while True:
        try:
            with urllib.request.urlopen(url) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            if error.code != 429:
                raise
            time.sleep(60)

def fetch_all(start_epoch=0, **params):
    """Yield every record from start_epoch onward, one response at a time."""
    while True:
        query = urllib.parse.urlencode({**params, "start-epoch": start_epoch}, doseq=True)
        page = get_page(f"{BASE}?{query}")

        yield from page["records"]

        if not page["hasMore"]:
            break

        start_epoch = page["nextStartEpoch"]

with open("fenceline.csv", "w", newline="", encoding="utf-8") as f:
    writer = None

    for record in fetch_all():
        if writer is None:
            writer = csv.DictWriter(f, fieldnames=list(record))
            writer.writeheader()

        writer.writerow(record)

To restrict it to one location, or to start somewhere other than the beginning, pass the same parameters the endpoint takes: fetch_all(**{"site-keys[]": ["path1"]}), or fetch_all(start_epoch=1785240000).

Both examples pause on the rate limit and carry on. At 120 requests per hour most retrievals never reach it: a year of one location runs about 12 requests, and eight months of all ten locations about 79. A multi-year retrieval across every location will reach it, and the loop then waits and resumes on its own. Handle it even if you expect never to hit it. A rejected request otherwise looks like the end of the data, and the loop stops early on a file that looks complete.

Example: shell

The same job with curl alone. In CSV mode the continuation values arrive as response headers, so nothing has to parse JSON. Each page after the first loses its header row, so the result is one well-formed CSV.

BASE="https://martinez.argos-scientific.com/api-fenceline-monitoring-data.php"
START=0
: > fenceline.csv

while : ; do
    STATUS=$(curl -s -D headers.txt -o page.csv -w '%{http_code}' "$BASE?format=csv&start-epoch=$START")

    # Rate limited: wait, then ask for the same window again.
    if [ "$STATUS" = "429" ]; then sleep 60; continue; fi
    # Any other non-200 has to stop the loop loudly. Carrying on would leave a
    # truncated file that looks complete.
    if [ "$STATUS" != "200" ]; then echo "HTTP $STATUS" >&2; exit 1; fi

    if [ -s fenceline.csv ]; then tail -n +2 page.csv >> fenceline.csv; else cat page.csv > fenceline.csv; fi

    grep -qi '^x-has-more: true' headers.txt || break
    START=$(grep -i '^x-next-start-epoch:' headers.txt | tr -dc '0-9')
done

Forgiving inputs

The API interprets the range parameters. How you express a request does not make it fail:

The API still rejects two inputs: an unknown location key and an unknown format. Guessing what either one meant would be worse than saying so.

ParameterRequiredDescription
site-keys[]No Repeatable. Restricts the response to the named monitoring locations. Omit it for all of them. Valid keys: sitea, siteb, sitec, sited, sitef, siteg, path1, path2, path3, path4.
start-epoch
end-epoch
No Unix timestamps in seconds, bounding the period requested. Either one works on its own, as described under Forgiving inputs above. Omit both to get the most recent hour, which suits polling for current data.
formatNo json (default) or csv.

The most recent hour for every location, as JSON:

GET /api-fenceline-monitoring-data.php
{
  "generatedAt": "2026-07-28T20:47:00+00:00",
  "sites": "Site A, Site B, Site C, Site D, Site F, Site G, Path 1, Path 2, Path 3, Path 4",
  "startEpoch": 1785268020,
  "endEpoch": 1785271620,
  "maxRangeSeconds": 267840,
  "truncated": false,
  "nextStartEpoch": 1785271620,
  "hasMore": false,
  "earliestEpoch": 1784966400,
  "records": [
    {
      "facility_name": "Martinez Refining Company",
      "instrument_id": "path1_uv",
      "instrument": "Path 1 UV-DOAS",
      "parameter": "Benzene",
      "date": "2026-07-28",
      "time": "12:40",
      "mean_concentration": 0.246609,
      "units_of_measure": "ppb",
      "averaging_period": 5,
      "observation_count": 1,
      "validity_indicator": "Y",
      "error_codes": "",
      "max_value": 0.246609,
      "required_loq": 0.9,
      "real_time_loq": 0.493218,
      "signal": 77131.3,
      "signal_units": "light count",
      "QC_code": "20",
      "final_data": "N",
      "change_log": ""
    }
  ]
}

One location, as CSV, for a specific period:

GET /api-fenceline-monitoring-data.php?site-keys[]=path1&format=csv
    &start-epoch=1785240000&end-epoch=1785243600

An unrecognised site-keys[] value or an unsupported format returns HTTP 400 with a plain-text explanation. Exceeding the rate limit returns HTTP 429.

Manual download

The website's Download Data page serves the same fields as CSV or XML, and covers historical data only: its range stops one averaging period before the present, and a request falling entirely inside that most recent period comes back with an explanation pointing to the API. The form asks for a captcha phrase, so a script should use the API above.

One location and up to 31 days per download. For a longer period, take consecutive downloads, or use the API, which walks the whole record for you. A request for more than 31 days returns the first 31; the file name states the period the file covers.

GLM Data

Five-minute records from the four ground level monitors. Field meanings are in the data dictionary. Records carry four fewer fields than the Fenceline Monitoring Data: no real_time_loq, signal, signal_units or QC_code.

ParameterDescription
glm-keys[]One or more of glm_ace, glm_mtview, glm_shellave, glm_etp. Omit for all four.
start-epochUnix timestamp, start of the range. Omit both bounds for the most recent hour.
end-epochUnix timestamp, end of the range.
formatjson or csv.

Ranges are handled the same way as the Fenceline Monitoring Data API above: a reversed range is read the way it was meant, a range longer than one response can carry is served up to the limit with hasMore and nextStartEpoch saying where to continue, and only an unknown monitor key or format is refused.

GET /api-glm-data.php?glm-keys[]=glm_ace&format=json
{
  "generatedAt": "2026-08-27T23:52:34+00:00",
  "sites": "Ace GLM",
  "startEpoch": 1787788800,
  "endEpoch": 1787789400,
  "nextStartEpoch": 1787789400,
  "hasMore": true,
  "records": [
    {
      "facility_name": "Martinez Refining Company",
      "instrument_id": "glm_ace",
      "instrument": "Ace GLM",
      "parameter": "Hydrogen Sulfide",
      "date": "2026-08-26",
      "time": "16:00",
      "mean_concentration": 2.4,
      "units_of_measure": "ppb",
      "averaging_period": 5,
      "observation_count": "",
      "validity_indicator": "Y",
      "error_codes": "",
      "max_value": 2.4,
      "required_loq": "",
      "final_data": "N",
      "change_log": ""
    }
  ]
}

Manual download

The Download Data page offers the same records as CSV or XML over a chosen date range. The monitors appear in the same location picker as the fenceline sites, one location per download, stopping five minutes short of the present as that download does.

Monitoring Locations

Where each monitoring instrument sits, and what it reports. There is no date range; these are fixed installation positions. Each published point gets one record: a point instrument, the met station and each ground level monitor have one apiece, and an open path has two. Both ends of a path share an instrument_id and are told apart by the instrument name.

The two open path instrument types are built differently, so the far end of a path is not the same thing in both cases. A TDL sends its beam to a corner cube reflector and reads the return at the analyzer, so its two points are Open Path Analyzer and Reflector. A UV-DOAS puts a lamp at one end and the analyzer at the other, with no reflector on the path at all, so its two points are Open Path Analyzer and Light Source. The analyzer end carries that name in both cases.

API

ParameterRequiredDescription
formatNo json (default), csv or geojson.

Manual download

The website's Download Data page carries these under Monitoring Locations, as CSV, GeoJSON or shapefile. The shapefile download is a zip holding the .shp, .shx, .dbf and .prj files, which have to travel together.

Each is also a plain link, so you can bookmark it or fetch it from a script:

Current Readings

This endpoint takes no parameters. Every request returns the same thing: the latest reading for every monitored location and property as of the moment of the request, covering every pollutant at every monitoring location plus every weather property at the met station. Ozone appears here; the Fenceline Monitoring Data leaves it out. There is no way to ask this endpoint for a specific location, property or date range. For those, use the Fenceline Monitoring Data API above, or the Download Data page.

GET /api-data.php
{
  "generatedAt": "2026-07-28T20:45:00+00:00",
  "records": [
    {
      "site": "path1",
      "siteName": "Path 1",
      "property": "ben",
      "propertyName": "Benzene",
      "unit": "ppb",
      "timestamp": "2026-07-28T20:45:00+00:00",
      "value": 0.246609,
      "loq": 0.493218,
      "description": "No Detection",
      "online": true
    },
    {
      "site": "met",
      "siteName": "Met",
      "property": "wind_speed",
      "propertyName": "Wind Speed",
      "unit": "mph",
      "timestamp": "2026-07-28T20:45:00+00:00",
      "value": 5.9,
      "loq": null,
      "description": null,
      "online": true
    }
  ]
}

The website's Learning Center defines the terms and acronyms used here, explains each data status label, and lists the current Limit of Quantification for every gas and monitoring location.

The Documents page holds the Fence Line Air Monitoring Plan, the Quality Assurance Project Plan and the quarterly monitoring reports.

The Download Data page provides the Fenceline Monitoring Data as CSV or XML over a chosen date range, stopping five minutes short of the present as above, and the Monitoring Locations as CSV, GeoJSON or shapefile.